import type { IncomingMessage, ServerResponse } from 'node:http'
import type {
  AppType,
  DocumentType,
  NextComponentType,
} from '../shared/lib/utils'
import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin'
import type {
  PageConfig,
  GetStaticPaths,
  GetServerSideProps,
  GetStaticProps,
} from '../types'
import type { RouteModule } from './route-modules/route-module'
import type { BuildManifest } from './get-page-files'
import type { ActionManifest } from '../build/webpack/plugins/flight-client-entry-plugin'

import {
  BUILD_MANIFEST,
  REACT_LOADABLE_MANIFEST,
  CLIENT_REFERENCE_MANIFEST,
  SERVER_REFERENCE_MANIFEST,
  DYNAMIC_CSS_MANIFEST,
  SUBRESOURCE_INTEGRITY_MANIFEST,
} from '../shared/lib/constants'
import { join } from 'path'
import { requirePage } from './require'
import { interopDefault } from '../lib/interop-default'
import { getTracer } from './lib/trace/tracer'
import { LoadComponentsSpan } from './lib/trace/constants'
import { evalManifest, loadManifest } from './load-manifest.external'
import { wait } from '../lib/wait'
import { setManifestsSingleton } from './app-render/manifests-singleton'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { isStaticMetadataRoute } from '../lib/metadata/is-metadata-route'

export type ManifestItem = {
  id: number | string
  files: string[]
}

export type ReactLoadableManifest = { [moduleId: string]: ManifestItem }
/**
 * This manifest prevents removing server rendered <link> tags after client
 * navigation. This is only needed under `Pages dir && Production && Webpack`.
 * @see https://github.com/vercel/next.js/pull/72959
 */
export type DynamicCssManifest = string[]

/**
 * A manifest entry type for the react-loadable-manifest.json.
 *
 * The whole manifest.json is a type of `Record<pathname, LoadableManifest>`
 * where pathname is a string-based key points to the path of the page contains
 * each dynamic imports.
 */
export interface LoadableManifest {
  [k: string]: { id: string | number; files: string[] }
}

export type GenericComponentMod = {
  handler(
    req: IncomingMessage,
    res: ServerResponse,
    ctx: {
      waitUntil?: (prom: Promise<void>) => void
    }
  ): Promise<void | null>
}

export type LoadComponentsReturnType<
  NextModule extends GenericComponentMod = GenericComponentMod,
> = {
  Component: NextComponentType
  pageConfig: PageConfig
  buildManifest: DeepReadonly<BuildManifest>
  subresourceIntegrityManifest?: DeepReadonly<Record<string, string>>
  reactLoadableManifest: DeepReadonly<ReactLoadableManifest>
  dynamicCssManifest?: DeepReadonly<DynamicCssManifest>
  Document: DocumentType
  App: AppType
  getStaticProps?: GetStaticProps
  getStaticPaths?: GetStaticPaths
  getServerSideProps?: GetServerSideProps
  ComponentMod: NextModule
  routeModule: RouteModule
  isAppPath?: boolean
  page: string
  multiZoneDraftMode?: boolean
}

/**
 * Load manifest file with retries, defaults to 3 attempts.
 */
export async function loadManifestWithRetries<T extends object>(
  manifestPath: string,
  attempts = 3
) {
  while (true) {
    try {
      return loadManifest<T>(manifestPath)
    } catch (err) {
      attempts--
      if (attempts <= 0) throw err

      await wait(100)
    }
  }
}

/**
 * Load manifest file with retries, defaults to 3 attempts, or return undefined.
 */
export async function tryLoadManifestWithRetries<T extends object>(
  manifestPath: string,
  attempts = 3
) {
  try {
    return await loadManifestWithRetries<T>(manifestPath, attempts)
  } catch (err) {
    return undefined
  }
}

/**
 * Load manifest file with retries, defaults to 3 attempts.
 */
export async function evalManifestWithRetries<T extends object>(
  manifestPath: string,
  attempts = 3
) {
  while (true) {
    try {
      return evalManifest<T>(manifestPath)
    } catch (err) {
      attempts--
      if (attempts <= 0) throw err

      await wait(100)
    }
  }
}

async function tryLoadClientReferenceManifest(
  manifestPath: string,
  entryName: string,
  attempts?: number
) {
  try {
    const context = await evalManifestWithRetries<{
      __RSC_MANIFEST: { [key: string]: ClientReferenceManifest }
    }>(manifestPath, attempts)
    return context.__RSC_MANIFEST[entryName]
  } catch (err) {
    return undefined
  }
}

async function loadComponentsImpl<
  N extends GenericComponentMod = GenericComponentMod,
>({
  distDir,
  page,
  isAppPath,
  isDev,
  sriEnabled,
  // When route modules are used, which is the case for the server calls to loadComponents, it no longer needs manifest to be loaded here.
  // Static generation still needs the manifests to be loaded here.
  // In the future static generation will also use route modules, and we will remove this flag.
  needsManifestsForLegacyReasons,
}: {
  distDir: string
  page: string
  isAppPath: boolean
  isDev: boolean
  sriEnabled: boolean
  needsManifestsForLegacyReasons: boolean
}): Promise<LoadComponentsReturnType<N>> {
  let DocumentMod = {}
  let AppMod = {}
  if (!isAppPath) {
    ;[DocumentMod, AppMod] = await Promise.all([
      requirePage('/_document', distDir, false),
      requirePage('/_app', distDir, false),
    ])
  }

  if (needsManifestsForLegacyReasons) {
    // In dev mode we retry loading a manifest file to handle a race condition
    // that can occur while app and pages are compiling at the same time, and the
    // build-manifest is still being written to disk while an app path is
    // attempting to load.
    const manifestLoadAttempts = isDev ? 3 : 1

    let reactLoadableManifestPath: string
    if (!process.env.TURBOPACK) {
      reactLoadableManifestPath = join(
        /* turbopackIgnore: true */ distDir,
        REACT_LOADABLE_MANIFEST
      )
    } else if (isAppPath) {
      reactLoadableManifestPath = join(
        /* turbopackIgnore: true */ distDir,
        'server',
        'app',
        page,
        REACT_LOADABLE_MANIFEST
      )
    } else {
      reactLoadableManifestPath = join(
        /* turbopackIgnore: true */ distDir,
        'server',
        'pages',
        normalizePagePath(page),
        REACT_LOADABLE_MANIFEST
      )
    }

    // Make sure to avoid loading the manifest for static metadata routes for better performance.
    const hasClientManifest = !isStaticMetadataRoute(page)

    // Load the manifest files first
    //
    // Loading page-specific manifests shouldn't throw an error if the manifest couldn't be found, so
    // that the `requirePage` call below will throw the correct error in that case
    // (a `PageNotFoundError`).
    const [
      buildManifest,
      reactLoadableManifest,
      dynamicCssManifest,
      clientReferenceManifest,
      serverActionsManifest,
      subresourceIntegrityManifest,
    ] = await Promise.all([
      loadManifestWithRetries<BuildManifest>(
        join(/* turbopackIgnore: true */ distDir, BUILD_MANIFEST),
        manifestLoadAttempts
      ),
      tryLoadManifestWithRetries<ReactLoadableManifest>(
        reactLoadableManifestPath,
        manifestLoadAttempts
      ),
      // This manifest will only exist in Pages dir && Production && Webpack.
      isAppPath || process.env.TURBOPACK
        ? undefined
        : loadManifestWithRetries<DynamicCssManifest>(
            join(
              /* turbopackIgnore: true */ distDir,
              `${DYNAMIC_CSS_MANIFEST}.json`
            ),
            manifestLoadAttempts
          ).catch(() => undefined),
      isAppPath && hasClientManifest
        ? tryLoadClientReferenceManifest(
            join(
              /* turbopackIgnore: true */ distDir,
              'server',
              'app',
              page.replace(/%5F/g, '_') +
                '_' +
                CLIENT_REFERENCE_MANIFEST +
                '.js'
            ),
            page.replace(/%5F/g, '_'),
            manifestLoadAttempts
          )
        : undefined,
      isAppPath
        ? loadManifestWithRetries<ActionManifest>(
            join(
              /* turbopackIgnore: true */ distDir,
              'server',
              SERVER_REFERENCE_MANIFEST + '.json'
            ),
            manifestLoadAttempts
          ).catch(() => null)
        : null,
      sriEnabled
        ? loadManifestWithRetries<DeepReadonly<Record<string, string>>>(
            join(
              /* turbopackIgnore: true */ distDir,
              'server',
              SUBRESOURCE_INTEGRITY_MANIFEST + '.json'
            )
          ).catch(() => undefined)
        : undefined,
    ])

    // Before requiring the actual page module, we have to set the reference
    // manifests to our global store so Server Action's encryption util can access
    // to them at the top level of the page module.
    if (serverActionsManifest && clientReferenceManifest) {
      setManifestsSingleton({
        page,
        clientReferenceManifest,
        serverActionsManifest,
      })
    }

    const ComponentMod = await requirePage(page, distDir, isAppPath)

    const Component = interopDefault(ComponentMod)
    const Document = interopDefault(DocumentMod)
    const App = interopDefault(AppMod)

    const { getServerSideProps, getStaticProps, getStaticPaths, routeModule } =
      ComponentMod

    return {
      App,
      Document,
      Component,
      buildManifest,
      subresourceIntegrityManifest,
      reactLoadableManifest: reactLoadableManifest || {},
      dynamicCssManifest,
      pageConfig: ComponentMod.config || {},
      ComponentMod,
      getServerSideProps,
      getStaticProps,
      getStaticPaths,
      isAppPath,
      page,
      routeModule,
    }
  } else {
    const ComponentMod = await requirePage(page, distDir, isAppPath)

    const Component = interopDefault(ComponentMod)
    const Document = interopDefault(DocumentMod)
    const App = interopDefault(AppMod)

    const { getServerSideProps, getStaticProps, getStaticPaths, routeModule } =
      ComponentMod

    return {
      App,
      Document,
      Component,
      pageConfig: ComponentMod.config || {},
      ComponentMod,
      getServerSideProps,
      getStaticProps,
      getStaticPaths,
      isAppPath,
      page,
      routeModule,
    } as any // temporary `as any` to make TypeScript not fail so that the tests will run on the PR.
  }
}

export const loadComponents = getTracer().wrap(
  LoadComponentsSpan.loadComponents,
  loadComponentsImpl
)
